#include <dirent.h>
#include <errno.h>
#include <grp.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#include <fcntl.h>
#include <getopt.h>
#include <poll.h>
#include <pthread.h>
#include <sys/time.h>

#include <libevdev/libevdev.h>
#include <libinput.h>
#include <libudev.h>

#include <X11/Xlib.h>
#include <X11/Xutil.h>

#define PROJECT_VERSION "0.1"
#define BUBBLE_PADDING 35
#define MIN_BUBBLE_WIDTH 220
#define MIN_BUBBLE_HEIGHT 80
#define BUBBLE_FONT "10x20"
#define MOUSE_OFFSET_X 20
#define MOUSE_OFFSET_Y -80
#define MAX_KEY_NAME 256
#define KEY_DISPLAY_DELAY 3
#define TIMER_INTERVAL 10000
#define MAX_DISPLAY_KEYS 1
#define AUDIO_FOLDER "/home/etcix/.config/xpet/audio/"

typedef struct {
  bool enable_audio;
  bool enable_random_audio;
  char *random_audio_folder;
  char *click_audio_to_play;
  uid_t original_uid;
  gid_t original_gid;
  char *original_home;
} Config;

Config config;

Display *dpy;
Window root;
Window bubble_win;
GC gc;
int scr;
int scr_width, scr_height;
char current_key[MAX_KEY_NAME * MAX_DISPLAY_KEYS] = {0};
pthread_mutex_t key_mutex = PTHREAD_MUTEX_INITIALIZER;
XFontStruct *global_font = NULL;
bool is_dragging = false;
int drag_offset_x = 0;
int drag_offset_y = 0;
bool manual_position_set = false;
int manual_window_x = 0;
int manual_window_y = 0;
pthread_t timer_thread;
bool timer_running = true;
pthread_mutex_t timer_mutex = PTHREAD_MUTEX_INITIALIZER;
time_t last_key_time = 0;

char key_sequence[MAX_DISPLAY_KEYS][MAX_KEY_NAME];
int sequence_index = 0;
pthread_mutex_t sequence_mutex = PTHREAD_MUTEX_INITIALIZER;

enum error_code {
  NO_ERROR,
  UDEV_FAILED,
  LIBINPUT_FAILED,
  SEAT_FAILED,
  PERMISSION_FAILED
};

struct input_handler_data {
  struct udev *udev;
  struct libinput *libinput;
};

bool is_audio_file(const char *filename) {
  const char *ext[] = {".mp3", ".wav", ".ogg", ".flac", NULL};
  for (int i = 0; ext[i]; i++) {
    if (strstr(filename, ext[i]) != NULL)
      return true;
  }
  return false;
}

char *random_audio_from_folder(const char *folder) {
  DIR *dir = opendir(folder);
  if (!dir) {
    fprintf(stderr, "Warning: Invalid audio folder '%s' - %s\n", folder,
            strerror(errno));
    return NULL;
  }

  struct dirent *entry;
  char **audio_files = NULL;
  int file_count = 0;

  while ((entry = readdir(dir)) != NULL) {
    if (entry->d_type != DT_REG)
      continue;
    if (is_audio_file(entry->d_name)) {
      audio_files = realloc(audio_files, sizeof(char *) * (file_count + 1));
      char *file_path = malloc(PATH_MAX);
      snprintf(file_path, PATH_MAX, "%s/%s", folder, entry->d_name);
      audio_files[file_count++] = file_path;
    }
  }
  closedir(dir);

  if (file_count == 0) {
    fprintf(stderr, "Warning: No audio files found in '%s'\n", folder);
    free(audio_files);
    return NULL;
  }

  int rand_idx = rand() % file_count;
  char *selected = strdup(audio_files[rand_idx]);

  for (int i = 0; i < file_count; i++)
    free(audio_files[i]);
  free(audio_files);

  return selected;
}

void play_click_audio(void) {
  if (!config.enable_audio)
    return;

  char *audio_path = NULL;

  if (config.enable_random_audio && config.random_audio_folder) {
    audio_path = random_audio_from_folder(config.random_audio_folder);
  }
  if (!audio_path || strlen(audio_path) == 0) {
    audio_path = strdup(config.click_audio_to_play);
  }

  if (!audio_path || strlen(audio_path) == 0 || access(audio_path, R_OK) != 0) {
    free(audio_path);
    return;
  }

  pid_t pid = fork();
  if (pid == 0) {
    if (setgid(config.original_gid) != 0) {
      fprintf(stderr, "Failed to set GID: %s\n", strerror(errno));
      exit(EXIT_FAILURE);
    }
    if (setuid(config.original_uid) != 0) {
      fprintf(stderr, "Failed to set UID: %s\n", strerror(errno));
      exit(EXIT_FAILURE);
    }

    setenv("HOME", config.original_home, 1);
    char pulse_server[PATH_MAX];
    snprintf(pulse_server, PATH_MAX, "/run/user/%d/pulse/native", config.original_uid);
    setenv("PULSE_SERVER", pulse_server, 1);

    execlp("ffplay", "ffplay", "-nodisp", "-autoexit", "-loglevel", "error",
           audio_path, (char *)NULL);
    exit(EXIT_FAILURE);
  } else if (pid < 0) {
    fprintf(stderr, "Failed to fork audio process: %s\n", strerror(errno));
  }

  free(audio_path);
}

void init_config() {
  config.enable_audio = true;
  config.enable_random_audio = true;
  config.click_audio_to_play = strdup("");
  config.random_audio_folder = strdup("/home/etcix/.config/xpet/audio/");
  config.original_home = NULL;

  const char *sudo_uid = getenv("SUDO_UID");
  const char *sudo_gid = getenv("SUDO_GID");
  if (sudo_uid && sudo_gid) {
    config.original_uid = atoi(sudo_uid);
    config.original_gid = atoi(sudo_gid);

    struct passwd *pw = getpwuid(config.original_uid);
    if (pw && pw->pw_dir) {
      config.original_home = strdup(pw->pw_dir);
    } else {
      config.original_home = strdup("/home/etcix");
    }
    printf("[DEBUG] Original user UID: %d, GID: %d, HOME: %s\n", config.original_uid, config.original_gid, config.original_home);
  } else {
    config.original_uid = getuid();
    config.original_gid = getgid();
    config.original_home = strdup(getenv("HOME") ?: "/home/etcix");
    fprintf(stderr, "[WARNING] Not run with sudo; using current UID/GID for audio\n");
  }
}

void update_key_sequence(const char *new_key) {
  pthread_mutex_lock(&sequence_mutex);
  pthread_mutex_lock(&key_mutex);

  strncpy(key_sequence[sequence_index], new_key, MAX_KEY_NAME - 1);
  key_sequence[sequence_index][MAX_KEY_NAME - 1] = '\0';
  sequence_index = (sequence_index + 1) % MAX_DISPLAY_KEYS;

  current_key[0] = '\0';
  int valid_count = 0;
  for (int i = 0; i < MAX_DISPLAY_KEYS; i++) {
    int idx = (sequence_index + i) % MAX_DISPLAY_KEYS;
    if (key_sequence[idx][0] == '\0')
      continue;

    if (valid_count > 0)
      strncat(current_key, " ", sizeof(current_key) - strlen(current_key) - 1);
    strncat(current_key, key_sequence[idx],
            sizeof(current_key) - strlen(current_key) - 1);
    valid_count++;
  }

  pthread_mutex_unlock(&key_mutex);
  pthread_mutex_unlock(&sequence_mutex);
}

void clear_key_sequence() {
  pthread_mutex_lock(&sequence_mutex);
  pthread_mutex_lock(&key_mutex);

  for (int i = 0; i < MAX_DISPLAY_KEYS; i++)
    key_sequence[i][0] = '\0';
  sequence_index = 0;
  current_key[0] = '\0';

  pthread_mutex_unlock(&key_mutex);
  pthread_mutex_unlock(&sequence_mutex);
}

void init_bubble_window() {
  dpy = XOpenDisplay(NULL);
  if (!dpy) {
    fprintf(stderr, "Failed to connect X server\n");
    exit(1);
  }

  scr = DefaultScreen(dpy);
  scr_width = DisplayWidth(dpy, scr);
  scr_height = DisplayHeight(dpy, scr);
  root = RootWindow(dpy, scr);
  printf("[DEBUG] Screen size: %dx%d\n", scr_width, scr_height);

  XSetWindowAttributes attr;
  attr.override_redirect = True;
  attr.background_pixel = WhitePixel(dpy, scr);
  attr.border_pixel = BlackPixel(dpy, scr);
  attr.event_mask =
      ExposureMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask;

  manual_window_x = scr_width / 2 - MIN_BUBBLE_WIDTH / 2;
  manual_window_y = scr_height / 2 - MIN_BUBBLE_HEIGHT / 2;
  bubble_win = XCreateWindow(
      dpy, root, manual_window_x, manual_window_y, MIN_BUBBLE_WIDTH,
      MIN_BUBBLE_HEIGHT, 3, CopyFromParent, InputOutput, CopyFromParent,
      CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &attr);
  printf("[DEBUG] Window created: pos(%d,%d) size(%dx%d)\n", manual_window_x,
         manual_window_y, MIN_BUBBLE_WIDTH, MIN_BUBBLE_HEIGHT);

  gc = XCreateGC(dpy, bubble_win, 0, NULL);
  if (!gc) {
    fprintf(stderr, "Failed to create GC\n");
    exit(1);
  }

  global_font = XLoadQueryFont(dpy, BUBBLE_FONT);
  if (!global_font) {
    fprintf(stderr, "Warning: Failed to load font '%s', try default...\n",
            BUBBLE_FONT);
    global_font = XLoadQueryFont(dpy, "9x15");
  }
  if (!global_font) {
    fprintf(stderr, "Fatal: No font available\n");
    exit(1);
  }
  printf("[DEBUG] Font loaded (height: % dpx)\n",
         global_font->ascent + global_font->descent);
  XSetFont(dpy, gc, global_font->fid);
  XSetForeground(dpy, gc, BlackPixel(dpy, scr));

  XMapWindow(dpy, bubble_win);
  XFlush(dpy);
  printf("[DEBUG] Window displayed\n");
}

void handle_drag_event(XEvent *ev) {
  if (!ev)
    return;
  switch (ev->type) {
  case ButtonPress:
    if (ev->xbutton.button == Button1) {
      is_dragging = true;
      drag_offset_x = ev->xbutton.x;
      drag_offset_y = ev->xbutton.y;
      printf("[DEBUG] Drag start: offset(%d,%d)\n", drag_offset_x,
             drag_offset_y);
    }
    break;
  case ButtonRelease:
    if (ev->xbutton.button == Button1) {
      is_dragging = false;
      manual_position_set = true;
      Window dummy;
      unsigned int width, height, border, depth;
      XGetGeometry(dpy, bubble_win, &dummy, &manual_window_x, &manual_window_y,
                   &width, &height, &border, &depth);
      printf("[DEBUG] Drag end: pos locked(%d,%d)\n", manual_window_x,
             manual_window_y);
    }
    break;
  case MotionNotify:
    if (is_dragging) {
      int new_x = ev->xmotion.x_root - drag_offset_x;
      int new_y = ev->xmotion.y_root - drag_offset_y;

      if (new_x < 0)
        new_x = 0;
      if (new_y < 0)
        new_y = 0;
      if (new_x + MIN_BUBBLE_WIDTH > scr_width)
        new_x = scr_width - MIN_BUBBLE_WIDTH;
      if (new_y + MIN_BUBBLE_HEIGHT > scr_height)
        new_y = scr_height - MIN_BUBBLE_HEIGHT;

      XMoveWindow(dpy, bubble_win, new_x, new_y);
      XFlush(dpy);
      printf("[DEBUG] Dragging: pos(%d,%d)\n", new_x, new_y);
    }
    break;
  default:
    break;
  }
}

void update_bubble() {
  pthread_mutex_lock(&key_mutex);
  if (current_key[0] == '\0') {
    XClearWindow(dpy, bubble_win);
    pthread_mutex_unlock(&key_mutex);
    return;
  }

  int bubble_x, bubble_y;
  if (manual_position_set) {
    bubble_x = manual_window_x;
    bubble_y = manual_window_y;
    printf("[DEBUG] Window pos: manual(%d,%d)\n", bubble_x, bubble_y);
  } else {
    Window root_ret, child_ret;
    int mouse_x, mouse_y, win_x, win_y;
    unsigned int mask_ret;
    if (XQueryPointer(dpy, root, &root_ret, &child_ret, &mouse_x, &mouse_y,
                      &win_x, &win_y, &mask_ret) != True) {
      mouse_x = scr_width / 2;
      mouse_y = scr_height / 2;
    }
    bubble_x = mouse_x + MOUSE_OFFSET_X;
    bubble_y = mouse_y + MOUSE_OFFSET_Y;
    printf("[DEBUG] Window pos: follow mouse(%d,%d)→(%d,%d)\n", mouse_x,
           mouse_y, bubble_x, bubble_y);
  }

  int text_width = XTextWidth(global_font, current_key, strlen(current_key));
  int text_height = global_font->ascent + global_font->descent;
  printf("[DEBUG] Text size: %s (w:%d h:%d)\n", current_key, text_width,
         text_height);

  int bubble_width = text_width + 2 * BUBBLE_PADDING;
  int bubble_height = text_height + 2 * BUBBLE_PADDING;
  if (bubble_width < MIN_BUBBLE_WIDTH)
    bubble_width = MIN_BUBBLE_WIDTH;
  if (bubble_height < MIN_BUBBLE_HEIGHT)
    bubble_height = MIN_BUBBLE_HEIGHT;
  if (bubble_width > scr_width / 2)
    bubble_width = scr_width / 2;
  if (bubble_height > scr_height / 4)
    bubble_height = scr_height / 4;

  if (bubble_x < 0)
    bubble_x = 0;
  if (bubble_y < 0)
    bubble_y = 0;
  if (bubble_x + bubble_width > scr_width)
    bubble_x = scr_width - bubble_width;
  if (bubble_y + bubble_height > scr_height)
    bubble_y = scr_height - bubble_height;

  XMoveResizeWindow(dpy, bubble_win, bubble_x, bubble_y, bubble_width,
                    bubble_height);
  printf("[DEBUG] Window resized: size(%dx%d) final pos(%d,%d)\n", bubble_width,
         bubble_height, bubble_x, bubble_y);

  XSetForeground(dpy, gc, WhitePixel(dpy, scr));
  XFillRectangle(dpy, bubble_win, gc, 0, 0, bubble_width, bubble_height);
  XSetForeground(dpy, gc, BlackPixel(dpy, scr));
  XDrawRectangle(dpy, bubble_win, gc, 0, 0, bubble_width - 1,
                 bubble_height - 1);
  int text_x = (bubble_width - text_width) / 2;
  int text_y = BUBBLE_PADDING + global_font->ascent;
  XDrawString(dpy, bubble_win, gc, text_x, text_y, current_key,
              strlen(current_key));
  XFlush(dpy);
  printf("[DEBUG] Bubble updated\n");

  pthread_mutex_unlock(&key_mutex);
}

static void *handle_input(void *user_data) {
  struct input_handler_data *data = user_data;
  char line[MAX_KEY_NAME];
  while (fgets(line, MAX_KEY_NAME, stdin) != NULL) {
    if (strcmp(line, "stop\n") == 0) {
      timer_running = false;
      pthread_cancel(timer_thread);
      pthread_join(timer_thread, NULL);
      if (global_font)
        XFreeFont(dpy, global_font);
      libinput_unref(data->libinput);
      udev_unref(data->udev);
      XFreeGC(dpy, gc);
      XDestroyWindow(dpy, bubble_win);
      XCloseDisplay(dpy);
      free(config.random_audio_folder);
      free(config.click_audio_to_play);
      free(config.original_home);
      printf("[DEBUG] Stop command received, exit\n");
      exit(EXIT_SUCCESS);
    }
  }
  return NULL;
}

static int open_restricted(const char *path, int flags, void *user_data) {
  int fd = open(path, flags);
  if (fd < 0)
    fprintf(stderr, "Failed to open device %s: %s\n", path, strerror(errno));
  else
    printf("[DEBUG] Device opened: %s (fd:%d)\n", path, fd);
  return fd < 0 ? -errno : fd;
}

static void close_restricted(int fd, void *user_data) {
  printf("[DEBUG] Device closed: fd=%d\n", fd);
  close(fd);
}

static const struct libinput_interface interface = {
    .open_restricted = open_restricted,
    .close_restricted = close_restricted,
};

static bool ctrl_pressed = false;
static bool shift_pressed = false;
static bool alt_pressed = false;
static bool win_pressed = false;

bool is_modifier_key(uint32_t key_code) {
  switch (key_code) {
  case 29:
  case 97:
  case 42:
  case 54:
  case 56:
  case 100:
  case 125:
  case 126:
    return true;
  default:
    return false;
  }
}

const char *get_modifier_name(uint32_t key_code) {
  switch (key_code) {
  case 29:
  case 97:
    return "Ctrl";
  case 42:
  case 54:
    return "Shift";
  case 56:
  case 100:
    return "Alt";
  case 125:
  case 126:
    return "WIN";
  default:
    return "";
  }
}

static void handle_key_event(struct libinput_event_keyboard *keyboard) {
  if (!keyboard)
    return;
  uint32_t key_code = libinput_event_keyboard_get_key(keyboard);
  enum libinput_key_state state =
      libinput_event_keyboard_get_key_state(keyboard);

  if (is_modifier_key(key_code)) {
    const char *mod_name = get_modifier_name(key_code);
    if (strcmp(mod_name, "Ctrl") == 0)
      ctrl_pressed = (state == LIBINPUT_KEY_STATE_PRESSED);
    else if (strcmp(mod_name, "Shift") == 0)
      shift_pressed = (state == LIBINPUT_KEY_STATE_PRESSED);
    else if (strcmp(mod_name, "Alt") == 0)
      alt_pressed = (state == LIBINPUT_KEY_STATE_PRESSED);
    else if (strcmp(mod_name, "WIN") == 0)
      win_pressed = (state == LIBINPUT_KEY_STATE_PRESSED);

    if (state == LIBINPUT_KEY_STATE_PRESSED) {
      char mod_key[MAX_KEY_NAME];
      snprintf(mod_key, sizeof(mod_key), "%s", mod_name);
      update_key_sequence(mod_key);

      pthread_mutex_lock(&timer_mutex);
      last_key_time = time(NULL);
      pthread_mutex_unlock(&timer_mutex);

      update_bubble();
      play_click_audio();
      printf("[Key Captured] %s (code: %d)\n", mod_name, key_code);
      fflush(stdout);
    }
    return;
  }

  if (state != LIBINPUT_KEY_STATE_PRESSED)
    return;

  printf("[DEBUG] Key captured: code=%d\n", key_code);
  const char *key_name = libevdev_event_code_get_name(EV_KEY, key_code);
  if (!key_name)
    key_name = "Unknown";

  const char *display_name = key_name;
  if (strcmp(key_name, "KEY_ENTER") == 0)
    display_name = "Enter";
  else if (strcmp(key_name, "KEY_BACKSPACE") == 0)
    display_name = "Backspace";
  else if (strcmp(key_name, "KEY_TAB") == 0)
    display_name = "Tab";
  else if (strcmp(key_name, "KEY_ESC") == 0)
    display_name = "Esc";
  else if (strcmp(key_name, "KEY_SPACE") == 0)
    display_name = "Space";
  else if (strcmp(key_name, "KEY_DELETE") == 0)
    display_name = "Del";
  else if (strcmp(key_name, "KEY_UP") == 0)
    display_name = "Up";
  else if (strcmp(key_name, "KEY_DOWN") == 0)
    display_name = "Down";
  else if (strcmp(key_name, "KEY_LEFT") == 0)
    display_name = "Left";
  else if (strcmp(key_name, "KEY_RIGHT") == 0)
    display_name = "Right";
  else if (strstr(key_name, "KEY_"))
    display_name = key_name + 4;

  char final_key[MAX_KEY_NAME] = {0};
  if (ctrl_pressed)
    strcat(final_key, "Ctrl + ");
  if (shift_pressed)
    strcat(final_key, "Shift + ");
  if (alt_pressed)
    strcat(final_key, "Alt + ");
  if (win_pressed)
    strcat(final_key, "WIN + ");
  strncat(final_key, display_name, sizeof(final_key) - strlen(final_key) - 1);

  update_key_sequence(final_key);

  pthread_mutex_lock(&timer_mutex);
  last_key_time = time(NULL);
  pthread_mutex_unlock(&timer_mutex);

  update_bubble();
  play_click_audio();
  printf("[Key Captured] %s (code: %d)\n", final_key, key_code);
  fflush(stdout);
}

static void handle_button_event(struct libinput_event_pointer *pointer) {
  if (!pointer)
    return;
  uint32_t btn_code = libinput_event_pointer_get_button(pointer);
  enum libinput_button_state state =
      libinput_event_pointer_get_button_state(pointer);
  if (state != LIBINPUT_BUTTON_STATE_PRESSED)
    return;

  const char *btn_name = libevdev_event_code_get_name(EV_KEY, btn_code);
  if (!btn_name)
    btn_name = "Unknown";
  printf("[DEBUG] Mouse button captured: code=%d name=%s\n", btn_code,
         btn_name);

  const char *display_name = btn_name;
  if (strcmp(btn_name, "BTN_LEFT") == 0)
    display_name = "Left";
  else if (strcmp(btn_name, "BTN_RIGHT") == 0)
    display_name = "Right";
  else if (strcmp(btn_name, "BTN_MIDDLE") == 0)
    display_name = "Middle";

  char final_key[MAX_KEY_NAME];
  snprintf(final_key, sizeof(final_key), "Mouse: %s", display_name);

  update_key_sequence(final_key);

  pthread_mutex_lock(&timer_mutex);
  last_key_time = time(NULL);
  pthread_mutex_unlock(&timer_mutex);

  update_bubble();
  play_click_audio();
  printf("[Mouse Captured] %s (code: %d)\n", final_key, btn_code);
  fflush(stdout);
}

static int handle_events(struct libinput *libinput) {
  if (!libinput)
    return -1;
  struct libinput_event *event;
  if (libinput_dispatch(libinput) < 0)
    return -1;

  while ((event = libinput_get_event(libinput)) != NULL) {
    printf("[DEBUG] libinput event received: type=%d\n",
           libinput_event_get_type(event));
    switch (libinput_event_get_type(event)) {
    case LIBINPUT_EVENT_KEYBOARD_KEY:
      handle_key_event(libinput_event_get_keyboard_event(event));
      break;
    case LIBINPUT_EVENT_POINTER_BUTTON:
      handle_button_event(libinput_event_get_pointer_event(event));
      break;
    default:
      break;
    }
    libinput_event_destroy(event);
  }
  return 0;
}

static void *key_timer_thread(void *arg) {
  (void)arg;
  while (timer_running) {
    usleep(TIMER_INTERVAL);
    pthread_mutex_lock(&timer_mutex);
    if (last_key_time > 0 &&
        (time(NULL) - last_key_time) >= KEY_DISPLAY_DELAY) {
      clear_key_sequence();
      XClearWindow(dpy, bubble_win);
      XFlush(dpy);
      printf("[DEBUG] Key sequence timeout (%ds), cleared\n",
             KEY_DISPLAY_DELAY);
      last_key_time = 0;
    }
    pthread_mutex_unlock(&timer_mutex);
  }
  return NULL;
}

static int run_mainloop(struct libinput *libinput) {
  if (!libinput)
    return -1;
  if (pthread_create(&timer_thread, NULL, key_timer_thread, NULL) != 0) {
    fprintf(stderr, "Timer thread create failed\n");
    return -1;
  }
  printf("[DEBUG] Timer thread started (timeout:%ds)\n", KEY_DISPLAY_DELAY);

  struct pollfd fd = {
      .fd = libinput_get_fd(libinput), .events = POLLIN, .revents = 0};
  printf("[DEBUG] libinput fd monitored: %d\n", fd.fd);

  pthread_mutex_lock(&key_mutex);
  snprintf(current_key, sizeof(current_key),
           "Press any key (drag to move) | Max keys: %d", MAX_DISPLAY_KEYS);
  pthread_mutex_unlock(&key_mutex);
  update_bubble();

  if (handle_events(libinput) != 0) {
    fprintf(stderr, "Event init failed, check sudo\n");
    return -1;
  }

  while (poll(&fd, 1, -1) > -1) {
    handle_events(libinput);
    while (XPending(dpy)) {
      XEvent ev;
      XNextEvent(dpy, &ev);
      printf("[DEBUG] X11 event received: type=%d\n", ev.type);
      if (ev.type == Expose && ev.xexpose.window == bubble_win) {
        update_bubble();
      } else {
        handle_drag_event(&ev);
      }
    }
  }
  return 0;
}

void print_help(char *program_name) {
  printf("keybubble-libinput - Global Key Bubble Display\n");
  printf("Version " PROJECT_VERSION "\n");
  printf("Usage: %s [OPTIONS]\n", program_name);
  printf("Options:\n");
  printf("\t-h, --help\tShow help\n");
  printf("\t-v, --version\tShow version\n");
  printf("Features:\n");
  printf("\t1. Capture global keys/mouse buttons (sequence display)\n");
  printf("\t2. Draggable window (left click)\n");
  printf("\t3. Auto-resize window (responsive)\n");
  printf("\t4. Auto-hide after %ds\n", KEY_DISPLAY_DELAY);
  printf("\t5. Custom max display keys (define MAX_DISPLAY_KEYS)\n");
  printf("\t6. Random audio playback from %s\n", AUDIO_FOLDER);
  printf("Note: Run with sudo\n");
}

int main(int argc, char *argv[]) {
  srand(time(NULL));
  init_config();

  const struct option long_options[] = {{"version", no_argument, 0, 'v'},
                                        {"help", no_argument, 0, 'h'},
                                        {NULL, 0, NULL, 0}};

  int opt;
  while ((opt = getopt_long(argc, argv, "vh", long_options, NULL)) != -1) {
    switch (opt) {
    case 'v':
      printf(PROJECT_VERSION "\n");
      free(config.random_audio_folder);
      free(config.click_audio_to_play);
      free(config.original_home);
      return 0;
    case 'h':
      print_help(argv[0]);
      free(config.random_audio_folder);
      free(config.click_audio_to_play);
      free(config.original_home);
      return 0;
    default:
      fprintf(stderr, "Invalid option: -%c\n", opt);
      free(config.random_audio_folder);
      free(config.click_audio_to_play);
      free(config.original_home);
      return 1;
    }
  }

  clear_key_sequence();
  init_bubble_window();

  struct udev *udev = udev_new();
  if (!udev) {
    fprintf(stderr, "udev init failed\n");
    free(config.random_audio_folder);
    free(config.click_audio_to_play);
    free(config.original_home);
    return UDEV_FAILED;
  }
  printf("[DEBUG] udev init success\n");

  struct libinput *libinput =
      libinput_udev_create_context(&interface, NULL, udev);
  if (!libinput) {
    fprintf(stderr, "libinput init failed\n");
    udev_unref(udev);
    free(config.random_audio_folder);
    free(config.click_audio_to_play);
    free(config.original_home);
    return LIBINPUT_FAILED;
  }
  printf("[DEBUG] libinput init success\n");

  if (libinput_udev_assign_seat(libinput, "seat0") != 0) {
    fprintf(stderr, "seat assign failed\n");
    libinput_unref(libinput);
    udev_unref(udev);
    free(config.random_audio_folder);
    free(config.click_audio_to_play);
    free(config.original_home);
    return SEAT_FAILED;
  }
  printf("[DEBUG] seat0 assigned\n");

  pthread_t input_thread;
  struct input_handler_data data = {udev, libinput};
  if (pthread_create(&input_thread, NULL, handle_input, &data) != 0) {
    fprintf(stderr, "Input thread create failed\n");
    libinput_unref(libinput);
    udev_unref(udev);
    free(config.random_audio_folder);
    free(config.click_audio_to_play);
    free(config.original_home);
    return PERMISSION_FAILED;
  }
  printf("[DEBUG] Input thread created (listen stop)\n");

  printf("\nStarted! Press any key (sequence display, max %d keys), drag "
         "window with left click, enter 'stop' to exit\n",
         MAX_DISPLAY_KEYS);
  if (run_mainloop(libinput) < 0) {
    timer_running = false;
    pthread_cancel(timer_thread);
    pthread_join(timer_thread, NULL);
    if (global_font)
      XFreeFont(dpy, global_font);
    libinput_unref(libinput);
    udev_unref(udev);
    free(config.random_audio_folder);
    free(config.click_audio_to_play);
    free(config.original_home);
    return PERMISSION_FAILED;
  }

  timer_running = false;
  pthread_cancel(timer_thread);
  pthread_join(timer_thread, NULL);
  if (global_font)
    XFreeFont(dpy, global_font);
  libinput_unref(libinput);
  udev_unref(udev);
  XFreeGC(dpy, gc);
  XDestroyWindow(dpy, bubble_win);
  XCloseDisplay(dpy);
  pthread_join(input_thread, NULL);
  free(config.random_audio_folder);
  free(config.click_audio_to_play);
  free(config.original_home);

  return NO_ERROR;
}
